/*
  由于 canvas 依赖系统底层需要编译且预编译包在 github releases 上，改用另一个纯 js 解码图片。若想继续使用 canvas 可调用 runWithCanvas 。

  添加 injectToRequest 用以快速修复需验证的请求。eg: $.get=injectToRequest($.get.bind($))
*/
const https = require( 'https' );
const http = require( 'http' );
const stream = require( 'stream' );
const zlib = require( 'zlib' );
const vm = require( 'vm' );
const PNG = require( 'png-js' );
const UA = require( './USER_AGENTS.js' ).USER_AGENT;


Math.avg = function average () {
  var sum = 0;
  var len = this.length;
  for ( var i = 0; i < len; i++ ) {
    sum += this[ i ];
  }
  return sum / len;
};

function sleep ( timeout ) {
  return new Promise( ( resolve ) => setTimeout( resolve, timeout ) );
}

class PNGDecoder extends PNG {
  constructor( args ) {
    super( args );
    this.pixels = [];
  }

  decodeToPixels () {
    return new Promise( ( resolve ) => {
      try {
        this.decode( ( pixels ) => {
          this.pixels = pixels;
          resolve();
        } );
      } catch ( e ) {
        console.info( e )
      }
    } );
  }

  getImageData ( x, y, w, h ) {
    const { pixels } = this;
    const len = w * h * 4;
    const startIndex = x * 4 + y * ( w * 4 );

    return { data: pixels.slice( startIndex, startIndex + len ) };
  }
}

const PUZZLE_GAP = 8;
const PUZZLE_PAD = 10;

class PuzzleRecognizer {
  constructor( bg, patch, y ) {
    // console.log(bg);
    const imgBg = new PNGDecoder( Buffer.from( bg, 'base64' ) );
    const imgPatch = new PNGDecoder( Buffer.from( patch, 'base64' ) );

    // console.log(imgBg);

    this.bg = imgBg;
    this.patch = imgPatch;
    this.rawBg = bg;
    this.rawPatch = patch;
    this.y = y;
    this.w = imgBg.width;
    this.h = imgBg.height;
  }

  async run () {
    try {
      await this.bg.decodeToPixels();
      await this.patch.decodeToPixels();

      return this.recognize();
    } catch ( e ) {
      console.info( e )
    }
  }

  recognize () {
    const { ctx, w: width, bg } = this;
    const { width: patchWidth, height: patchHeight } = this.patch;
    const posY = this.y + PUZZLE_PAD + ( ( patchHeight - PUZZLE_PAD ) / 2 ) - ( PUZZLE_GAP / 2 );
    // const cData = ctx.getImageData(0, a.y + 10 + 20 - 4, 360, 8).data;
    const cData = bg.getImageData( 0, posY, width, PUZZLE_GAP ).data;
    const lumas = [];

    for ( let x = 0; x < width; x++ ) {
      var sum = 0;

      // y xais
      for ( let y = 0; y < PUZZLE_GAP; y++ ) {
        var idx = x * 4 + y * ( width * 4 );
        var r = cData[ idx ];
        var g = cData[ idx + 1 ];
        var b = cData[ idx + 2 ];
        var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;

        sum += luma;
      }

      lumas.push( sum / PUZZLE_GAP );
    }

    const n = 2; // minium macroscopic image width (px)
    const margin = patchWidth - PUZZLE_PAD;
    const diff = 20; // macroscopic brightness difference
    const radius = PUZZLE_PAD;
    for ( let i = 0, len = lumas.length - 2 * 4; i < len; i++ ) {
      const left = ( lumas[ i ] + lumas[ i + 1 ] ) / n;
      const right = ( lumas[ i + 2 ] + lumas[ i + 3 ] ) / n;
      const mi = margin + i;
      const mLeft = ( lumas[ mi ] + lumas[ mi + 1 ] ) / n;
      const mRigth = ( lumas[ mi + 2 ] + lumas[ mi + 3 ] ) / n;

      if ( left - right > diff && mLeft - mRigth < -diff ) {
        const pieces = lumas.slice( i + 2, margin + i + 2 );
        const median = pieces.sort( ( x1, x2 ) => x1 - x2 )[ 20 ];
        const avg = Math.avg( pieces );

        // noise reducation
        if ( median > left || median > mRigth ) return;
        if ( avg > 100 ) return;
        // console.table({left,right,mLeft,mRigth,median});
        // ctx.fillRect(i+n-radius, 0, 1, 360);
        // console.log(i+n-radius);
        return i + n - radius;
      }
    }

    // not found
    return -1;
  }

  runWithCanvas () {
    const { createCanvas, Image } = require( 'canvas' );
    const canvas = createCanvas();
    const ctx = canvas.getContext( '2d' );
    const imgBg = new Image();
    const imgPatch = new Image();
    const prefix = 'data:image/png;base64,';

    imgBg.src = prefix + this.rawBg;
    imgPatch.src = prefix + this.rawPatch;
    const { naturalWidth: w, naturalHeight: h } = imgBg;
    canvas.width = w;
    canvas.height = h;
    ctx.clearRect( 0, 0, w, h );
    ctx.drawImage( imgBg, 0, 0, w, h );

    const width = w;
    const { naturalWidth, naturalHeight } = imgPatch;
    const posY = this.y + PUZZLE_PAD + ( ( naturalHeight - PUZZLE_PAD ) / 2 ) - ( PUZZLE_GAP / 2 );
    // const cData = ctx.getImageData(0, a.y + 10 + 20 - 4, 360, 8).data;
    const cData = ctx.getImageData( 0, posY, width, PUZZLE_GAP ).data;
    const lumas = [];

    for ( let x = 0; x < width; x++ ) {
      var sum = 0;

      // y xais
      for ( let y = 0; y < PUZZLE_GAP; y++ ) {
        var idx = x * 4 + y * ( width * 4 );
        var r = cData[ idx ];
        var g = cData[ idx + 1 ];
        var b = cData[ idx + 2 ];
        var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;

        sum += luma;
      }

      lumas.push( sum / PUZZLE_GAP );
    }

    const n = 2; // minium macroscopic image width (px)
    const margin = naturalWidth - PUZZLE_PAD;
    const diff = 20; // macroscopic brightness difference
    const radius = PUZZLE_PAD;
    for ( let i = 0, len = lumas.length - 2 * 4; i < len; i++ ) {
      const left = ( lumas[ i ] + lumas[ i + 1 ] ) / n;
      const right = ( lumas[ i + 2 ] + lumas[ i + 3 ] ) / n;
      const mi = margin + i;
      const mLeft = ( lumas[ mi ] + lumas[ mi + 1 ] ) / n;
      const mRigth = ( lumas[ mi + 2 ] + lumas[ mi + 3 ] ) / n;

      if ( left - right > diff && mLeft - mRigth < -diff ) {
        const pieces = lumas.slice( i + 2, margin + i + 2 );
        const median = pieces.sort( ( x1, x2 ) => x1 - x2 )[ 20 ];
        const avg = Math.avg( pieces );

        // noise reducation
        if ( median > left || median > mRigth ) return;
        if ( avg > 100 ) return;
        // console.table({left,right,mLeft,mRigth,median});
        // ctx.fillRect(i+n-radius, 0, 1, 360);
        // console.log(i+n-radius);
        return i + n - radius;
      }
    }

    // not found
    return -1;
  }
}

const DATA = {
  "appId": "17839d5db83",
  "product": "embed",
  "lang": "zh_CN",
};
const SERVER = '61.49.99.122';

class JDJRValidator {
  constructor() {
    this.data = {};
    this.x = 0;
    this.t = Date.now();
  }

  async run ( scene ) {
    try {
      const tryRecognize = async () => {
        const x = await this.recognize( scene );

        if ( x > 0 ) {
          return x;
        }
        // retry
        return await tryRecognize();
      };
      const puzzleX = await tryRecognize();
      // console.log(puzzleX);
      const pos = new MousePosFaker( puzzleX ).run();
      const d = getCoordinate( pos );

      // console.log(pos[pos.length-1][2] -Date.now());
      // await sleep(4500);
      await sleep( pos[ pos.length - 1 ][ 2 ] - Date.now() );
      const result = await JDJRValidator.jsonp( '/slide/s.html', { d, ...this.data }, scene );

      if ( result.message === 'success' ) {
        // console.log(result);
        console.log( 'JDJR验证用时: %fs', ( Date.now() - this.t ) / 1000 );
        return result;
      } else {
        console.count( "验证失败" );
        // console.count(JSON.stringify(result));
        await sleep( 300 );
        return await this.run( scene );
      }
    } catch ( e ) {
      console.info( e )
    }
  }

  async recognize ( scene ) {
    try {
      const data = await JDJRValidator.jsonp( '/slide/g.html', { e: '' }, scene );
      const { bg, patch, y } = data;
      // const uri = 'data:image/png;base64,';
      // const re = new PuzzleRecognizer(uri+bg, uri+patch, y);
      const re = new PuzzleRecognizer( bg, patch, y );
      const puzzleX = await re.run();

      if ( puzzleX > 0 ) {
        this.data = {
          c: data.challenge,
          w: re.w,
          e: '',
          s: '',
          o: '',
        };
        this.x = puzzleX;
      }
      return puzzleX;
    } catch ( e ) {
      console.info( e )
    }
  }

  async report ( n ) {
    console.time( 'PuzzleRecognizer' );
    let count = 0;

    for ( let i = 0; i < n; i++ ) {
      const x = await this.recognize();

      if ( x > 0 ) count++;
      if ( i % 50 === 0 ) {
        // console.log('%f\%', (i / n) * 100);
      }
    }

    console.log( '验证成功: %f\%', ( count / n ) * 100 );
    console.timeEnd( 'PuzzleRecognizer' );
  }

  static jsonp ( api, data = {}, scene ) {
    return new Promise( ( resolve, reject ) => {
      const fnId = `jsonp_${ String( Math.random() ).replace( '.', '' ) }`;
      const extraData = { callback: fnId };
      const query = new URLSearchParams( { ...DATA, ...{ "scene": scene }, ...extraData, ...data } ).toString();
      const url = `http://${ SERVER }${ api }?${ query }`;
      const headers = {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip,deflate,br',
        'Accept-Language': 'zh-CN,en-US',
        'Connection': 'keep-alive',
        'Host': SERVER,
        'Proxy-Connection': 'keep-alive',
        'Referer': 'https://h5.m.jd.com/babelDiy/Zeus/2wuqXrZrhygTQzYA7VufBEpj4amH/index.html',
        'User-Agent': UA,
      };
      const req = http.get( url, { headers }, ( response ) => {
        let res = response;
        if ( res.headers[ 'content-encoding' ] === 'gzip' ) {
          const unzipStream = new stream.PassThrough();
          stream.pipeline(
            response,
            zlib.createGunzip(),
            unzipStream,
            reject,
          );
          res = unzipStream;
        }
        res.setEncoding( 'utf8' );

        let rawData = '';

        res.on( 'data', ( chunk ) => rawData += chunk );
        res.on( 'end', () => {
          try {
            const ctx = {
              [ fnId ]: ( data ) => ctx.data = data,
              data: {},
            };

            vm.createContext( ctx );
            vm.runInContext( rawData, ctx );

            // console.log(ctx.data);
            res.resume();
            resolve( ctx.data );
          } catch ( e ) {
            reject( e );
          }
        } );
      } );

      req.on( 'error', reject );
      req.end();
    } );
  }
}

function getCoordinate ( c ) {
  function string10to64 ( d ) {
    var c = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-~".split( "" )
      , b = c.length
      , e = +d
      , a = [];
    do {
      mod = e % b;
      e = ( e - mod ) / b;
      a.unshift( c[ mod ] )
    } while ( e );
    return a.join( "" )
  }

  function prefixInteger ( a, b ) {
    return ( Array( b ).join( 0 ) + a ).slice( -b )
  }

  function pretreatment ( d, c, b ) {
    var e = string10to64( Math.abs( d ) );
    var a = "";
    if ( !b ) {
      a += ( d > 0 ? "1" : "0" )
    }
    a += prefixInteger( e, c );
    return a
  }

  var b = new Array();
  for ( var e = 0; e < c.length; e++ ) {
    if ( e == 0 ) {
      b.push( pretreatment( c[ e ][ 0 ] < 262143 ? c[ e ][ 0 ] : 262143, 3, true ) );
      b.push( pretreatment( c[ e ][ 1 ] < 16777215 ? c[ e ][ 1 ] : 16777215, 4, true ) );
      b.push( pretreatment( c[ e ][ 2 ] < 4398046511103 ? c[ e ][ 2 ] : 4398046511103, 7, true ) )
    } else {
      var a = c[ e ][ 0 ] - c[ e - 1 ][ 0 ];
      var f = c[ e ][ 1 ] - c[ e - 1 ][ 1 ];
      var d = c[ e ][ 2 ] - c[ e - 1 ][ 2 ];
      b.push( pretreatment( a < 4095 ? a : 4095, 2, false ) );
      b.push( pretreatment( f < 4095 ? f : 4095, 2, false ) );
      b.push( pretreatment( d < 16777215 ? d : 16777215, 4, true ) )
    }
  }
  return b.join( "" )
}

const HZ = 25;

class MousePosFaker {
  constructor( puzzleX ) {
    this.x = parseInt( Math.random() * 20 + 20, 10 );
    this.y = parseInt( Math.random() * 80 + 80, 10 );
    this.t = Date.now();
    this.pos = [ [ this.x, this.y, this.t ] ];
    this.minDuration = parseInt( 1000 / HZ, 10 );
    // this.puzzleX = puzzleX;
    this.puzzleX = puzzleX + parseInt( Math.random() * 2 - 1, 10 );

    this.STEP = parseInt( Math.random() * 6 + 5, 10 );
    this.DURATION = parseInt( Math.random() * 7 + 14, 10 ) * 100;
    // [9,1600] [10,1400]
    this.STEP = 9;
    // this.DURATION = 2000;
    // console.log(this.STEP, this.DURATION);
  }

  run () {
    const perX = this.puzzleX / this.STEP;
    const perDuration = this.DURATION / this.STEP;
    const firstPos = [ this.x - parseInt( Math.random() * 6, 10 ), this.y + parseInt( Math.random() * 11, 10 ), this.t ];

    this.pos.unshift( firstPos );
    this.stepPos( perX, perDuration );
    this.fixPos();

    const reactTime = parseInt( 60 + Math.random() * 100, 10 );
    const lastIdx = this.pos.length - 1;
    const lastPos = [ this.pos[ lastIdx ][ 0 ], this.pos[ lastIdx ][ 1 ], this.pos[ lastIdx ][ 2 ] + reactTime ];

    this.pos.push( lastPos );
    return this.pos;
  }

  stepPos ( x, duration ) {
    let n = 0;
    const sqrt2 = Math.sqrt( 2 );
    for ( let i = 1; i <= this.STEP; i++ ) {
      n += 1 / i;
    }
    for ( let i = 0; i < this.STEP; i++ ) {
      x = this.puzzleX / ( n * ( i + 1 ) );
      const currX = parseInt( ( Math.random() * 30 - 15 ) + x, 10 );
      const currY = parseInt( Math.random() * 7 - 3, 10 );
      const currDuration = parseInt( ( Math.random() * 0.4 + 0.8 ) * duration, 10 );

      this.moveToAndCollect( {
        x: currX,
        y: currY,
        duration: currDuration,
      } );
    }
  }

  fixPos () {
    const actualX = this.pos[ this.pos.length - 1 ][ 0 ] - this.pos[ 1 ][ 0 ];
    const deviation = this.puzzleX - actualX;

    if ( Math.abs( deviation ) > 4 ) {
      this.moveToAndCollect( {
        x: deviation,
        y: parseInt( Math.random() * 8 - 3, 10 ),
        duration: 250,
      } );
    }
  }

  moveToAndCollect ( { x, y, duration } ) {
    let movedX = 0;
    let movedY = 0;
    let movedT = 0;
    const times = duration / this.minDuration;
    let perX = x / times;
    let perY = y / times;
    let padDuration = 0;

    if ( Math.abs( perX ) < 1 ) {
      padDuration = duration / Math.abs( x ) - this.minDuration;
      perX = 1;
      perY = y / Math.abs( x );
    }

    while ( Math.abs( movedX ) < Math.abs( x ) ) {
      const rDuration = parseInt( padDuration + Math.random() * 16 - 4, 10 );

      movedX += perX + Math.random() * 2 - 1;
      movedY += perY;
      movedT += this.minDuration + rDuration;

      const currX = parseInt( this.x + movedX, 10 );
      const currY = parseInt( this.y + movedY, 10 );
      const currT = this.t + movedT;

      this.pos.push( [ currX, currY, currT ] );
    }

    this.x += x;
    this.y += y;
    this.t += Math.max( duration, movedT );
  }
}

// new JDJRValidator().run();
// new JDJRValidator().report(1000);
// console.log(getCoordinate(new MousePosFaker(100).run()));

function injectToRequest2 ( fn, scene = 'cww' ) {
  return ( opts, cb ) => {
    fn( opts, async ( err, resp, data ) => {
      try {
        if ( err ) {
          console.error( '验证请求失败.' );
          return;
        }
        if ( data.search( '验证' ) > -1 ) {
          console.log( 'JDJR验证中......' );
          const res = await new JDJRValidator().run( scene );
          if ( res ) {
            opts.url += `&validate=${ res.validate }`;
          }
          fn( opts, cb );
        } else {
          cb( err, resp, data );
        }
      } catch ( e ) {
        console.info( e )
      }
    } );
  };
}

async function injectToRequest ( scene = 'cww' ) {
  console.log( 'JDJR验证中......' );
  const res = await new JDJRValidator().run( scene );
  return `&validate=${ res.validate }`
}

module.exports = {
  sleep,
  injectToRequest,
  injectToRequest2
}
